﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TopDownOperatorPrecedence
{


    public enum PTokenType
    {
        Name, String, Number, Operator,
    }

    public class PToken
    {
        public string Value { get; private set; }
        public PTokenType Type { get; private set; }
        public int From { get; private set; }
        public int To { get; private set; }

        public PToken(string value, PTokenType type, int from, int to)
        {
            this.Value = value;
            this.Type = type;
            this.From = from;
            this.To = to;
        }

        public override string ToString()
        {
            return string.Format("<{0}, {1}, {2}, {3}>", this.Type, this.Value, this.From, this.To);
        }

        private static readonly string[] Operators = new string[] { 
            "+", "-", "*", "/", "%", // Arithmetic operator
            "+=", "-=", "*=", "/=", "%=", // Inplace arithmetic operator
            "<", ">", "==", "<=", ">=", "!=", // Comparative operator
            "&&", "||", "!", // Logical operator
            "&", "|", "~", "^", // Bitwise operator
            "&=", "|=", "~=", "^=", // Inplace bitwise operator
            "?", ":", "[", "]", "(", ")", "{", "}", ".", // Conditional/Index/Function/Block/Dot operator
            ";", ",", // Statement operator
            "=", // Assignment operator
            "=>", // Lambda operator
        };

        private static readonly string VariablePrefixes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$";
        private static readonly string VariableCharacters = VariablePrefixes + "0123456789";

        private static readonly string OperatorPrefixes = string.Join(string.Empty, Operators.Select(s => s[0]).Distinct());

        private static readonly string NumberPrefixes = "0123456789";

        public static List<PToken> TokensFromString(string input, string operatorPrefixes = null)
        {
            if (input == null)
            {
                throw new Exception("Input could not be null");
            }

            if (operatorPrefixes == null)
            {
                operatorPrefixes = OperatorPrefixes;
            }

            List<PToken> result = new List<PToken>();
            // The main tokenizer loop
            int index = 0;
            while (index < input.Length)
            {
                char currentChr = input[index];
                if (char.IsWhiteSpace(currentChr)) // Skip white space characters
                {
                    for (; index < input.Length; ++index)
                    {
                        currentChr = input[index];
                        if (!char.IsWhiteSpace(currentChr))
                        {
                            break;
                        }
                    }
                }
                else if (NumberPrefixes.Contains(currentChr)) // Process number
                {
                    // TODO: Deal with float number later
                    StringBuilder sb = new StringBuilder();
                    for (; index < input.Length; ++index)
                    {
                        currentChr = input[index];
                        if (NumberPrefixes.Contains(currentChr))
                        {
                            sb.Append(currentChr);
                        }
                        else
                        {
                            break;
                        }
                    }
                    result.Add(new PToken(sb.ToString(), PTokenType.Number, index - sb.Length, index));
                }
                else if (currentChr == '\'' || currentChr == '\"') // Process string
                {
                    try
                    {
                        char stringDelim = currentChr;
                        index += 1;
                        currentChr = input[index];
                        StringBuilder sb = new StringBuilder();
                        while (currentChr != stringDelim)
                        {
                            if (currentChr == '\\') // Process escaped character
                            {
                                index += 1;
                                currentChr = input[index];
                                switch (currentChr)
                                {
                                    case '\'':
                                    case '\"':
                                        sb.Append(currentChr);
                                        break;
                                    case 'r':
                                        sb.Append('\r');
                                        break;
                                    case 'n':
                                        sb.Append('\n');
                                        break;
                                    case 't':
                                        sb.Append('\t');
                                        break;
                                    default:
                                        break;
                                }
                            }
                            else // Process normal character
                            {
                                sb.Append(currentChr);
                            }
                            // Go forward
                            index += 1;
                            currentChr = input[index];
                        }
                        index += 1;
                        result.Add(new PToken(sb.ToString(), PTokenType.String, index - 2 - sb.Length, index));
                    }
                    catch (IndexOutOfRangeException ex)
                    {
                        throw new Exception("String not closed", ex);
                    }
                }
                else if (operatorPrefixes.Contains(currentChr)) // Process operator
                {
                    if (index + 1 < input.Length) // look up two-char operator first, then one-char operator
                    {
                        string op = currentChr.ToString() + input[index + 1].ToString();
                        if (Operators.Contains(op))
                        {
                            result.Add(new PToken(op, PTokenType.Operator, index, index + 2));
                            index += 2;
                        }
                        else
                        {
                            op = currentChr.ToString();
                            if (Operators.Contains(op))
                            {
                                result.Add(new PToken(op, PTokenType.Operator, index, index + 1));
                                index += 1;
                            }
                            else
                            {
                                throw new Exception("Unrecoginized operator " + currentChr);
                            }
                        }
                    }
                    else // if reaches end of string, just look up one-char operator
                    {
                        if (Operators.Contains(currentChr.ToString()))
                        {
                            result.Add(new PToken(currentChr.ToString(), PTokenType.Operator, index, index + 1));
                            index += 1;
                        }
                        else
                        {
                            throw new Exception("Unrecoginized operator " + currentChr);
                        }
                    }
                }
                else if (VariablePrefixes.Contains(currentChr)) // Process name
                {
                    StringBuilder sb = new StringBuilder();
                    sb.Append(currentChr);
                    for (index += 1; index < input.Length; index++)
                    {
                        currentChr = input[index];
                        if (VariableCharacters.Contains(currentChr))
                        {
                            sb.Append(currentChr);
                        }
                        else
                        {
                            break;
                        }
                    }
                    result.Add(new PToken(sb.ToString(), PTokenType.Name, index - sb.Length, index));
                }
                else
                {
                    throw new Exception("Unrecoginized character " + currentChr + " at position " + index);
                }
            }

            return result;
        }
    }
}
